import * as uuid from 'uuid';
import * as z from 'zod';

import db from '@nangohq/database';

import { PROD_ENVIRONMENT_NAME } from '../constants.js';
import { configService, externalWebhookService, getGlobalOAuthCallbackUrl } from '../index.js';
import { LogActionEnum } from '../models/Telemetry.js';
import encryptionManager, { pbkdf2 } from '../utils/encryption.manager.js';
import errorManager, { ErrorSourceEnum } from '../utils/error.manager.js';

import type { Orchestrator } from '../index.js';
import type { DBEnvironment, DBEnvironmentVariable, SdkLogger } from '@nangohq/types';

const TABLE = '_nango_environments';

export const defaultEnvironments = [PROD_ENVIRONMENT_NAME, 'dev'];

class EnvironmentService {
    async getEnvironmentsByAccountId(account_id: number): Promise<Pick<DBEnvironment, 'id' | 'name'>[]> {
        try {
            const result = await db.knex
                .select<Pick<DBEnvironment, 'name' | 'id'>[]>('id', 'name')
                .from<DBEnvironment>(TABLE)
                .where({ account_id, deleted: false })
                .orderBy('name', 'asc');

            if (result == null || result.length == 0) {
                return [];
            }

            return result;
        } catch (err) {
            errorManager.report(err, {
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.DATABASE,
                accountId: account_id
            });

            return [];
        }
    }

    async getById(id: number): Promise<DBEnvironment | null> {
        const raw = await this.getRawById(id);
        return encryptionManager.decryptEnvironment(raw);
    }

    async getRawById(id: number): Promise<DBEnvironment | null> {
        try {
            const result = await db.knex.select('*').from<DBEnvironment>(TABLE).where({ id, deleted: false });

            if (result == null || result.length == 0 || result[0] == null) {
                return null;
            }

            return result[0];
        } catch (err) {
            errorManager.report(err, {
                environmentId: id,
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.DATABASE,
                metadata: {
                    id
                }
            });
            return null;
        }
    }

    async getByEnvironmentName(accountId: number, name: string): Promise<DBEnvironment | null> {
        const result = await db.knex.select('*').from<DBEnvironment>(TABLE).where({ account_id: accountId, name, deleted: false });

        if (result == null || result.length == 0 || result[0] == null) {
            return null;
        }

        return encryptionManager.decryptEnvironment(result[0]);
    }

    async createEnvironment(accountId: number, name: string): Promise<DBEnvironment | null> {
        const [environment] = await db.knex.from<DBEnvironment>(TABLE).insert({ account_id: accountId, name }).returning('*');

        if (!environment) {
            return null;
        }

        const encryptedEnvironment = await encryptionManager.encryptEnvironment({
            ...environment,
            secret_key_hashed: await hashSecretKey(environment.secret_key)
        });
        await db.knex.from<DBEnvironment>(TABLE).where({ id: environment.id }).update(encryptedEnvironment);

        const env = encryptionManager.decryptEnvironment(encryptedEnvironment);
        return env;
    }

    async createDefaultEnvironments(accountId: number): Promise<void> {
        for (const environment of defaultEnvironments) {
            const newEnv = await this.createEnvironment(accountId, environment);
            if (newEnv) {
                await externalWebhookService.update(newEnv.id, {
                    on_auth_creation: true,
                    on_auth_refresh_error: true,
                    on_sync_completion_always: true,
                    on_sync_error: true
                });
            }
        }
    }

    async getEnvironmentsWithOtlpSettings(): Promise<DBEnvironment[]> {
        const result = await db.knex.select('*').from<DBEnvironment>(TABLE).where({ deleted: false }).whereNotNull('otlp_settings');
        if (result == null) {
            return [];
        }
        return result.map((env) => encryptionManager.decryptEnvironment(env));
    }

    async getEnvironmentsByIds(environmentIds: number[]): Promise<DBEnvironment[]> {
        if (environmentIds.length === 0) {
            return [];
        }
        const result = await db.knex.select('*').from<DBEnvironment>(TABLE).whereIn('id', environmentIds).andWhere({ deleted: false });
        if (!result) {
            return [];
        }
        return result;
    }

    async getSlackNotificationsEnabled(environmentId: number, trx = db.knex): Promise<boolean | null> {
        const result = await trx.select('slack_notifications').from<DBEnvironment>(TABLE).where({ id: environmentId, deleted: false });

        if (result == null || result.length == 0 || result[0] == null) {
            return null;
        }

        return result[0].slack_notifications;
    }

    async update({
        accountId,
        environmentId,
        data
    }: {
        accountId: number;
        environmentId: number;
        data: Omit<Partial<DBEnvironment>, 'account_id' | 'id' | 'created_at' | 'updated_at'>;
    }): Promise<DBEnvironment | null> {
        const [res] = await db.knex.from<DBEnvironment>(TABLE).where({ account_id: accountId, id: environmentId, deleted: false }).update(data).returning('*');
        return res || null;
    }

    async getEnvironmentVariables(environment_id: number): Promise<DBEnvironmentVariable[]> {
        const result = await db.knex.select('*').from<DBEnvironmentVariable>(`_nango_environment_variables`).where({ environment_id });

        if (result === null || result.length === 0) {
            return [];
        }

        return encryptionManager.decryptEnvironmentVariables(result);
    }

    async editEnvironmentVariable(environment_id: number, values: { name: string; value: string }[]): Promise<number[] | null> {
        await db.knex.from<DBEnvironmentVariable>(`_nango_environment_variables`).where({ environment_id }).del();

        if (values.length === 0) {
            return null;
        }

        const mappedValues: Omit<DBEnvironmentVariable, 'id'>[] = values.map((value) => {
            return {
                ...value,
                created_at: new Date(),
                updated_at: new Date(),
                environment_id,
                value_iv: null,
                value_tag: null
            };
        });

        const encryptedValues = encryptionManager.encryptEnvironmentVariables(mappedValues);

        const results = await db.knex.from<DBEnvironmentVariable>(`_nango_environment_variables`).where({ environment_id }).insert(encryptedValues);

        if (results === null || results.length === 0) {
            return null;
        }

        return results;
    }

    async rotateKey(id: number, type: string): Promise<string | null> {
        if (type === 'secret') {
            return this.rotateSecretKey(id);
        }

        if (type === 'public') {
            return this.rotatePublicKey(id);
        }

        return null;
    }

    async revertKey(id: number, type: string): Promise<string | null> {
        if (type === 'secret') {
            return this.revertSecretKey(id);
        }

        if (type === 'public') {
            return this.revertPublicKey(id);
        }

        return null;
    }

    async activateKey(id: number, type: string): Promise<boolean> {
        if (type === 'secret') {
            return this.activateSecretKey(id);
        }

        if (type === 'public') {
            return this.activatePublicKey(id);
        }

        return false;
    }

    async rotateSecretKey(id: number): Promise<string | null> {
        const environment = await this.getById(id);

        if (!environment) {
            return null;
        }

        const pending_secret_key = uuid.v4();

        await db.knex.from<DBEnvironment>(TABLE).where({ id }).update({ pending_secret_key });

        environment.pending_secret_key = pending_secret_key;

        const encryptedEnvironment = await encryptionManager.encryptEnvironment(environment);
        await db.knex.from<DBEnvironment>(TABLE).where({ id }).update(encryptedEnvironment);

        return pending_secret_key;
    }

    async rotatePublicKey(id: number): Promise<string | null> {
        const pending_public_key = uuid.v4();

        await db.knex.from<DBEnvironment>(TABLE).where({ id }).update({ pending_public_key });

        return pending_public_key;
    }

    async revertSecretKey(id: number): Promise<string | null> {
        const environment = await this.getById(id);

        if (!environment) {
            return null;
        }

        await db.knex.from<DBEnvironment>(TABLE).where({ id }).update({
            pending_secret_key: null,
            pending_secret_key_iv: null,
            pending_secret_key_tag: null
        });

        return environment.secret_key;
    }

    async revertPublicKey(id: number): Promise<string | null> {
        const environment = await this.getById(id);

        if (!environment) {
            return null;
        }

        await db.knex.from<DBEnvironment>(TABLE).where({ id }).update({ pending_public_key: null });

        return environment.public_key;
    }

    async activateSecretKey(id: number): Promise<boolean> {
        const environment = await this.getRawById(id);
        if (!environment) {
            return false;
        }

        const decrypted = encryptionManager.decryptEnvironment(environment);
        await db.knex
            .from<DBEnvironment>(TABLE)
            .where({ id })
            .update({
                secret_key: environment.pending_secret_key as string,
                secret_key_iv: environment.pending_secret_key_iv as string,
                secret_key_tag: environment.pending_secret_key_tag as string,
                secret_key_hashed: await hashSecretKey(decrypted.pending_secret_key!),
                pending_secret_key: null,
                pending_secret_key_iv: null,
                pending_secret_key_tag: null
            });

        const updatedEnvironment = await this.getById(id);

        if (!updatedEnvironment) {
            return false;
        }

        return true;
    }

    async activatePublicKey(id: number): Promise<boolean> {
        const environment = await this.getById(id);

        if (!environment) {
            return false;
        }

        await db.knex
            .from<DBEnvironment>(TABLE)
            .where({ id })
            .update({
                public_key: environment.pending_public_key as string,
                pending_public_key: null
            });

        return true;
    }

    async getOauthCallbackUrl(environmentId?: number): Promise<string> {
        const globalCallbackUrl = getGlobalOAuthCallbackUrl();

        if (environmentId != null) {
            // TODO: remove this call
            const environment: DBEnvironment | null = await this.getById(environmentId);
            return environment?.callback_url || globalCallbackUrl;
        }

        return globalCallbackUrl;
    }
    async getSdkLogger(id: number): Promise<SdkLogger> {
        const defaultLevel = 'warn';
        const levelSchema = z.enum(['debug', 'info', 'warn', 'error', 'off']).default(defaultLevel);
        const envVars = await this.getEnvironmentVariables(id);
        const parsed = levelSchema.safeParse(envVars.find((ev) => ev.name === 'NANGO_LOGGER_LEVEL')?.value);
        const level = parsed.success ? parsed.data : defaultLevel; // fallback to default if parsing fails
        return { level };
    }

    async softDelete({ environmentId, orchestrator }: { environmentId: number; orchestrator: Orchestrator }): Promise<void> {
        const configs = await configService.listProviderConfigs(db.knex, environmentId);
        for (const config of configs) {
            // This handles deleting connections and syncs down the line
            await configService.deleteProviderConfig({
                id: config.id!,
                environmentId,
                providerConfigKey: config.unique_key,
                orchestrator
            });
        }

        await db.knex.from<DBEnvironment>(TABLE).where({ id: environmentId, deleted: false }).update({ deleted: true, deleted_at: new Date() });
    }

    async getSoftDeleted({ limit, olderThan }: { limit: number; olderThan: number }): Promise<DBEnvironment[]> {
        const dateThreshold = new Date();
        dateThreshold.setDate(dateThreshold.getDate() - olderThan);

        return await db.knex
            .select('*')
            .from<DBEnvironment>(`_nango_environments`)
            .where('deleted', true)
            .andWhere('deleted_at', '<=', dateThreshold.toISOString())
            .limit(limit);
    }

    async hardDelete(id: number): Promise<number> {
        return await db.knex.from<DBEnvironment>(TABLE).where({ id }).delete();
    }
}

export async function hashSecretKey(key: string) {
    if (!encryptionManager.getKey()) {
        return key;
    }

    return (await pbkdf2(key, encryptionManager.getKey(), 310000, 32, 'sha256')).toString('base64');
}

export default new EnvironmentService();
